﻿using JESAI.DistributedLock.Internal;
using JESAI.DistributedLock.Redis.Primitives;
using JESAI.DistributedLock.Redis.RedLock;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.Redis;

/// <summary>
/// 使用Redis实现<see cref="IDistributedReaderWriterLock"/>。可以通过RedLock算法利用多个服务器。
/// </summary>
public sealed partial class RedisDistributedReaderWriterLock : IInternalDistributedReaderWriterLock<RedisDistributedReaderWriterLockHandle>
{
    private readonly IReadOnlyList<IDatabase> _databases;
    private readonly RedisDistributedLockOptions _options;

    /// <summary>
    /// 使用提供的<paramref name="database"/>和<paramref name="options"/>构造一个名为<paramref name="name"/>的锁。
    /// </summary>
    public RedisDistributedReaderWriterLock(string name, IDatabase database, Action<RedisDistributedSynchronizationOptionsBuilder>? options = null)
        : this(name, new[] { database ?? throw new ArgumentNullException(nameof(database)) }, options)
    {
    }

    /// <summary>
    /// 使用提供的<paramref name="databases"/>和<paramref name="options"/>构造一个名为<paramref name="name"/>的锁。
    /// </summary>
    public RedisDistributedReaderWriterLock(string name, IEnumerable<IDatabase> databases, Action<RedisDistributedSynchronizationOptionsBuilder>? options = null)
    {
        if (name == null) { throw new ArgumentNullException(nameof(name)); }
        this._databases = RedisDistributedLock.ValidateDatabases(databases);

        this.ReaderKey = name + ".readers";
        this.WriterKey = name + ".writer";
        this.Name = name;
        this._options = RedisDistributedSynchronizationOptionsBuilder.GetOptions(options);

        // 我们坚持这个规则，以确保当我们获取写等待锁时，它不会在尝试升级为写锁的过程中过期。这样就避免了需要延长写等待锁的时间。
        if (this._options.RedLockTimeouts.MinValidityTime.CompareTo(this._options.MaxBusyWaitSleepTime) <= 0)
        {
            throw new ArgumentException($"{nameof(RedisDistributedSynchronizationOptionsBuilder.BusyWaitSleepTime)}必须 <= {nameof(RedisDistributedSynchronizationOptionsBuilder.MinValidityTime)}", nameof(options));
        }
    }

    internal RedisKey ReaderKey { get; }
    internal RedisKey WriterKey { get; }

    /// <summary>
    /// 实现<see cref="IDistributedReaderWriterLock.Name"/>
    /// </summary>
    public string Name { get; }

    ValueTask<RedisDistributedReaderWriterLockHandle?> IInternalDistributedReaderWriterLock<RedisDistributedReaderWriterLockHandle>.InternalTryAcquireAsync(
        TimeoutValue timeout,
        CancellationToken cancellationToken,
        bool isWrite)
    {
        return isWrite
            ? this.TryAcquireWriteLockAsync(timeout, cancellationToken)
            : BusyWaitHelper.WaitAsync(
                this,
                (@lock, cancellationToken) => @lock.TryAcquireAsync(new RedisReadLockPrimitive(@lock.ReaderKey, @lock.WriterKey, @lock._options.RedLockTimeouts), cancellationToken),
                timeout: timeout,
                minSleepTime: this._options.MinBusyWaitSleepTime,
                maxSleepTime: this._options.MaxBusyWaitSleepTime,
                cancellationToken
            );
    }

    private async ValueTask<RedisDistributedReaderWriterLockHandle?> TryAcquireWriteLockAsync(TimeoutValue timeout, CancellationToken cancellationToken)
    {
        var acquireWriteLockState = new AcquireWriteLockState(canRetry: !timeout.IsZero);
        RedisDistributedReaderWriterLockHandle? handle = null;
        try
        {
            return handle = await BusyWaitHelper.WaitAsync(
               (Lock: this, State: acquireWriteLockState),
               (state, cancellationToken) => state.Lock.TryAcquireWriteLockAsync(state.State, cancellationToken),
               timeout: timeout,
               minSleepTime: this._options.MinBusyWaitSleepTime,
               maxSleepTime: this._options.MaxBusyWaitSleepTime,
               cancellationToken
            ).ConfigureAwait(false);
        }
        finally
        {
            // 如果我们未能获取写锁，但获取了写等待锁，在退出时释放写等待锁。
            if (handle == null && acquireWriteLockState.WriterWaiting.TryGetValue(out var writerWaiting))
            {
                await new RedLockRelease(writerWaiting.Primitive, writerWaiting.TryAcquireTasks).ReleaseAsync().ConfigureAwait(false);
            }
        }
    }

    private async ValueTask<RedisDistributedReaderWriterLockHandle?> TryAcquireWriteLockAsync(AcquireWriteLockState state, CancellationToken cancellationToken)
    {
        // 第一次尝试时，直接尝试获取写锁。这涵盖了TryAcquire(0)的情况，并确保如果不需要，我们不会费心去获取写等待锁。
        if (state.IsFirstTry)
        {
            state.IsFirstTry = false;
            var firstTryResult = await TryAcquireWriteLockAsync(RedLockHelper.CreateLockId()).ConfigureAwait(false);
            if (firstTryResult != null) { return firstTryResult; }
            // 如果我们不打算重试获取，就不要尝试获取写等待锁
            if (!state.CanRetry) { return null; }
        }

        Invariant.Require(state.CanRetry);

        // 否则，如果我们还没有写等待锁，尝试获取它
        if (!state.WriterWaiting.HasValue)
        {
            var lockId = RedLockHelper.CreateLockId();
            var primitive = new RedisWriterWaitingPrimitive(this.WriterKey, lockId, this._options.RedLockTimeouts);
            var tryAcquireTasks = await new RedLockAcquire(primitive, this._databases, cancellationToken).TryAcquireAsync().ConfigureAwait(false);
            if (tryAcquireTasks == null) { return null; }

            // 如果我们获取了写等待锁，保存信息并继续
            state.WriterWaiting = (primitive, tryAcquireTasks, lockId);
        }

        // 如果我们到了这里，说明我们已经有了写等待锁。尝试将其“升级”为实际的写锁
        return await TryAcquireWriteLockAsync(state.WriterWaiting.Value.LockId).ConfigureAwait(false);

        ValueTask<RedisDistributedReaderWriterLockHandle?> TryAcquireWriteLockAsync(RedisValue lockId) =>
            this.TryAcquireAsync(new RedisWriteLockPrimitive(this.ReaderKey, this.WriterKey, lockId, this._options.RedLockTimeouts), cancellationToken);
    }

    private async ValueTask<RedisDistributedReaderWriterLockHandle?> TryAcquireAsync<TPrimitive>(TPrimitive primitive, CancellationToken cancellationToken)
        where TPrimitive : IRedLockAcquirableSynchronizationPrimitive, IRedLockExtensibleSynchronizationPrimitive
    {
        var tryAcquireTasks = await new RedLockAcquire(primitive, this._databases, cancellationToken).TryAcquireAsync().ConfigureAwait(false);
        return tryAcquireTasks != null
            ? new RedisDistributedReaderWriterLockHandle(new RedLockHandle(primitive, tryAcquireTasks, extensionCadence: this._options.ExtensionCadence, expiry: this._options.RedLockTimeouts.Expiry))
            : null;
    }

    private class AcquireWriteLockState
    {
        public AcquireWriteLockState(bool canRetry)
        {
            this.CanRetry = canRetry;
        }

        public bool CanRetry { get; }

        public bool IsFirstTry { get; set; } = true;

        public (RedisWriterWaitingPrimitive Primitive, IReadOnlyDictionary<IDatabase, Task<bool>> TryAcquireTasks, RedisValue LockId)? WriterWaiting { get; set; }
    }
}
